Event Scripting
Event scripting is a preview feature in 2.2.0. Read the enforcement boundaries below before using it as an operational control.
Melodee can run administrator-authored JavaScript at selected events. Scripts execute in-process with Jint and return either a boolean or an object containing a boolean result.
function check(ctx, scriptConfig) {
return { result: true, message: "Optional message" };
}
true allows the action and false denies it. A missing or disabled script, empty body, syntax/runtime error, timeout, statement-limit failure, or unsupported return value fails open and allows the action.
Manage scripts
Open Admin → Scripts. Administrators can create, edit, test, enable, disable, and delete a setting for one of the registered events. Settings use the key:
script.{eventName}
For example, script.directoryProcessingStart.
The editor’s test action evaluates the script against administrator-supplied mock JSON. A passing test confirms the script contract, not that every production entry point enforces that event.
Script contract
Melodee invokes:
check(ctx, scriptConfig)
An expression without a function check declaration is wrapped automatically:
ctx.mediaFilesCount >= 3
Supported returns are:
| Return | Effect |
|---|---|
true |
Allow |
false |
Deny |
{ result: false, message: "Reason" } |
Deny and provide a UI message |
Context and configuration properties are exposed with camel-case names.
scriptConfig currently contains:
| Property | Meaning |
|---|---|
eventName |
Registered event name |
settingKey |
Database setting key |
timeoutMs |
Jint timeout |
maxStatements |
Jint statement limit |
onDeny |
Default directory deny action |
Scripts do not receive live .NET objects or direct filesystem/network APIs. The Jint engine uses strict mode, a default 50 ms timeout, and a default 10,000-statement limit.
Current configuration boundaries
A stored configuration has this shape:
{
"version": 1,
"enabled": true,
"engine": "jint",
"timeoutMs": 50,
"maxStatements": 10000,
"default": {
"enabled": true,
"onDeny": "skip",
"body": "function check(ctx, scriptConfig) { return true; }"
},
"overrides": []
}
In 2.2.0:
- only the top-level
enabled, limits,default.body, anddefault.onDenyaffect execution; engineis stored, but execution always uses Jint;default.enabledis stored but is not consulted;- library/path overrides can be edited and are counted in the UI, but the orchestration service does not select them.
Use one default body per event. Do not rely on overrides until the runtime connects the override selector.
Enforced events
directoryProcessingStart
This is the server-side enforcement hook. It runs before each stable candidate directory is processed by the Inbound pipeline.
ctx contains:
| Property | Type |
|---|---|
path |
string |
directoryName |
string |
totalFilesCount |
number |
totalSizeMegabytes |
number |
mostRecentModified |
string |
mediaFilesCount |
number |
totalDurationMinutes |
number |
trackNumbers |
number[] |
hasTrackNumberGaps |
boolean |
When the result is false:
skipleaves the directory in place and skips processing;quarantinemoves it toscript.quarantine.path;deleteis deliberately converted toskipin the ingestion path.
The quarantine destination must be writable and should not be inside Inbound. script.dryRun.enabled protects the separate safe-delete service, but it does not suppress quarantine moves.
function check(ctx, scriptConfig) {
if (ctx.mediaFilesCount < 3) {
return { result: false, message: "Release has fewer than three media files." };
}
return true;
}
directoryProcessingDelete remains a registered legacy name, but the ingestion pipeline does not evaluate it.
Web-page gating events
These events run when a Blazor page initializes:
| Event | Current effect |
|---|---|
userRegistrationStart |
Disables the registration form |
userLoginStart |
Disables the login form |
userProfileUpdateStart |
Makes the profile page read-only |
playlistCreateStart |
Disables both playlist-import buttons |
podcastChannelAddStart |
Disables Add Channel |
requestCreateStart |
Disables New Request |
They are UI gates, not service/API authorization policies. Native and OpenSubsonic requests do not evaluate them.
Several context values are placeholders because evaluation happens before form submission:
| Event | Values available at evaluation |
|---|---|
| registration | userNameLength=0, emailDomain="", plus IP, user agent, and time |
| login | userId=null, roles=[], plus IP, user agent, and time |
| profile | actual user ID and email domain; profileChangesCount=0 |
| playlist create | actual user ID; nameLength=0, initialSongCount=0 |
| podcast add | actual user ID; feedUrl="", isNewSubscription=true |
| request create | actual user ID; requestType="", isFirstRequestToday=true, dailyRequestCount=0 |
Do not write rules that claim to validate playlist names, podcast feed URLs, submitted registration emails, or daily request totals with those placeholders.
One valid page-level use is a time-based maintenance gate:
function check(ctx, scriptConfig) {
const hour = new Date(ctx.now).getUTCHours();
return hour < 2 || hour >= 3
? true
: { result: false, message: "Maintenance is in progress from 02:00–03:00 UTC." };
}
Operational guidance
- Keep scripts small and deterministic.
- Test with allow, deny, malformed, timeout, and missing-property cases.
- Review logs by setting key and script hash; Melodee avoids logging the full body on evaluation failures.
- Pair UI gates with real authorization or configuration controls when bypass would matter.
- Back up PostgreSQL before large script-setting changes.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.